Skip to content

19.0.preparation#2

Closed
weinni2000 wants to merge 1 commit into19.0.integrationfrom
19.0.preparation
Closed

19.0.preparation#2
weinni2000 wants to merge 1 commit into19.0.integrationfrom
19.0.preparation

Conversation

@weinni2000
Copy link
Owner

@weinni2000 weinni2000 commented Oct 4, 2025

Summary by Sourcery

Upgrade base_geoengine for Odoo 19 by refactoring search condition generation to support geospatial operators in domains, updating imports and APIs for compatibility, improving WKT parsing resilience, aligning tests with the new search API, and refreshing documentation and manifest metadata for version 19.0

New Features:

  • Enable geospatial operators in Odoo search by monkey-patching Field._condition_to_sql and DomainCondition to handle geo_greater, geo_lesser, geo_equal, geo_touch, geo_within, geo_contains, and geo_intersect operators

Enhancements:

  • Refactor codebase for Odoo 19 compatibility: replace deprecated expression API with odoo.fields.Domain, odoo.orm.domains utilities, Query, SQL, and Domain.AND
  • Improve geo WKT parsing in geo_convertion_helper with exception handling and fallback to an empty POINT
  • Standardize translation calls to use env._ and update logging references

Documentation:

  • Bump module version to 19.0.1.0.1 in manifest and update static description badges, repository links, and issue template URLs for version 19.0

Tests:

  • Adjust test cases to use limit parameter in search calls and rename test methods to align with Odoo 19 search API

Chores:

  • Add a new domains.py to replace deprecated base_geoengine/expressions.py and consolidate geospatial operator support

@sourcery-ai
Copy link

sourcery-ai bot commented Oct 4, 2025

Reviewer's Guide

This PR migrates base_geoengine to Odoo 19 by replacing the old expression API with the new Domain/Query framework and integrating geospatial operators into Field._condition_to_sql and DomainCondition, updates imports and translation calls, adds robust WKT parsing fallback, bumps module version, updates static assets and tests.

Class diagram for geospatial operator integration in DomainCondition and Field

classDiagram
    class DomainCondition {
        +field_expr: str
        +operator: str
        +value
        +_opt_level: OptimizationLevel
        +checked()
        +_to_sql(model, alias, query)
        +_optimize_step(model, level)
    }
    class Field {
        +_condition_to_sql(field_expr, operator, value, model, alias, query)
    }
    class GeoOperator {
        +get_geo_greater_sql(...)
        +get_geo_lesser_sql(...)
        +get_geo_equal_sql(...)
        +get_geo_touch_sql(...)
        +get_geo_within_sql(...)
        +get_geo_contains_sql(...)
        +get_geo_intersect_sql(...)
    }
    class GeoField {
        +geo_type
        +srid
        +dim
        +update_geo_db_column(model)
        +update_db_column(model, column)
        +entry_to_shape(value, same_type)
    }
    Field <|-- GeoField
    DomainCondition --> Field : uses
    Field --> GeoOperator : instantiates
    GeoField --> GeoOperator : instantiates
Loading

Class diagram for new domains.py monkey patching and operator extension

classDiagram
    class Domain {
        +AND(domains)
    }
    class DomainCondition {
        +checked()
        +_to_sql(model, alias, query)
        +_optimize_step(model, level)
    }
    class domains {
        +CONDITION_OPERATORS
    }
    DomainCondition --> domains : extends CONDITION_OPERATORS
    Domain --> DomainCondition : uses
Loading

File-Level Changes

Change Details Files
Integrate geo operators into the new Domain/Query API
  • Define GEO_SQL_OPERATORS and GEO_OPERATORS maps
  • Implement get_geo_func() to dispatch SQL builder calls
  • Add where_calc() using Domain.optimize_full() and Query.add_where()
  • Monkey-patch Field.condition_to_sql to handle geo operators via GeoOperator
  • Create domains.py to extend DomainCondition (checked, _to_sql, _optimize_step) for geo operators
  • Add GEO_OPERATORS to global CONDITION_OPERATORS
base_geoengine/fields.py
base_geoengine/domains.py
Clean up legacy expressions and imports
  • Remove old expressions.py monkey-patch logic and imports
  • Comment out TERM_OPERATORS adjustments
  • Adjust imports to drop odoo.osv.expression and use odoo.fields.Domain
base_geoengine/expressions.py
Update translation and API usage for Odoo 19
  • Replace ('…') with self.env.(…) in Python modules
  • Switch model._cr to model.env.cr
  • Adjust security XML to use user_ids instead of users
base_geoengine/models/base.py
base_geoengine/geo_db.py
base_geoengine/geo_convertion_helper.py
base_geoengine/security/data.xml
Bump module metadata and static docs to 19.0
  • Update manifest.py version to 19.0.1.0.1
  • Fix GitHub and Weblate links in index.html to target 19.0 branch
base_geoengine/__manifest__.py
base_geoengine/static/description/index.html
Enhance WKT parsing error handling
  • Add fallback in value_to_shape() to catch exceptions and return an empty point
  • Introduce logger import in geo_convertion_helper
base_geoengine/geo_convertion_helper.py
Adjust tests to limit queries for deterministic results
  • Rename one test method to generic name
  • Add limit=1 to several search() calls in tests
base_geoengine/tests/test_model.py
UI/JS minor fix
  • Change default groupBy property from object to array in geoengine_renderer
base_geoengine/static/src/js/views/geoengine/geoengine_renderer/geoengine_renderer.esm.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The direct monkey-patching of core methods like Field._condition_to_sql and DomainCondition._to_sql/_optimize_step is quite complex and brittle—consider refactoring into a single, centralized extension or using official hooks to reduce risk.
  • You have duplicated definitions of GEO_OPERATORS and GEO_SQL_OPERATORS in multiple files—extract them into one shared module to avoid drift and simplify maintenance.
  • The value_to_shape function silently falls back to an empty POINT(0.0 0.0) on parse errors, which can mask real data issues—consider surfacing errors or handling invalid WKT more explicitly instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The direct monkey-patching of core methods like Field._condition_to_sql and DomainCondition._to_sql/_optimize_step is quite complex and brittle—consider refactoring into a single, centralized extension or using official hooks to reduce risk.
- You have duplicated definitions of GEO_OPERATORS and GEO_SQL_OPERATORS in multiple files—extract them into one shared module to avoid drift and simplify maintenance.
- The value_to_shape function silently falls back to an empty POINT(0.0 0.0) on parse errors, which can mask real data issues—consider surfacing errors or handling invalid WKT more explicitly instead.

## Individual Comments

### Comment 1
<location> `base_geoengine/fields.py:312` </location>
<code_context>
     def update_geo_db_column(self, model):
         """Update the column type in the database."""
-        cr = model._cr
+        cr = model.env.cr
         query = """SELECT srid, type, coord_dimension
                  FROM geometry_columns
</code_context>

<issue_to_address>
**question (bug_risk):** Switching from model._cr to model.env.cr may have side effects.

Please verify that all usages of this method are compatible with model.env.cr and do not rely on a different cursor.
</issue_to_address>

### Comment 2
<location> `base_geoengine/geo_convertion_helper.py:29-38` </location>
<code_context>
+from .geo_operators import GeoOperator

 logger = logging.getLogger(__name__)
 try:
</code_context>

<issue_to_address>
**issue (bug_risk):** Returning a default geometry on WKT parse failure may mask data issues.

Instead of returning a default geometry, raise an exception or log the error to make data issues visible and easier to debug.
</issue_to_address>

### Comment 3
<location> `base_geoengine/models/geo_vector_layer.py:6` </location>
<code_context>
 # Copyright 2023 ACSONE SA/NV
 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

-from odoo import _, api, fields, models
+from odoo import api, fields, models
 from odoo.exceptions import ValidationError

</code_context>

<issue_to_address>
**suggestion (bug_risk):** Removing '_' import requires all translations to use self.env._.

Check for any remaining uses of '_' for translations and update them to 'self.env._' to prevent runtime errors.

Suggested implementation:

```python
                    raise ValidationError(
                        self.env._(

```

If there are other uses of `_(` for translations elsewhere in this file, they should also be replaced with `self.env._(`. Please review the entire file to ensure all translation calls use `self.env._`.
</issue_to_address>

### Comment 4
<location> `base_geoengine/fields.py:36` </location>
<code_context>
     )
-
-
-def get_geo_func(current_operator, operator, left, value, params, table):
-    """
-    This method will call the SQL query corresponding to the requested geo operator
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the geo operator logic by merging mappings, using dynamic dispatch, and extracting repeated code into helpers.

```suggestion
Most of this boilerplate can be collapsed into a few small helpers. In particular:

 1. Remove the huge `match` in `get_geo_func` by dynamic dispatch.
 2. Merge `GEO_OPERATORS` and `GEO_SQL_OPERATORS` into one mapping.
 3. Replace your custom `where_calc` with directly using `Domain._to_sql`.
 4. Factor out the repeated random‐alias/subquery logic into a helper.

For example:

```python
# 1. Single OPERATOR map for both the SQL symbol and the GeoOperator method suffix
GEO_OPERATORS = {
    "geo_greater": { "sql": SQL(">"), "method": "get_geo_greater_sql" },
    "geo_lesser":  { "sql": SQL("<"), "method": "get_geo_lesser_sql" },
    "geo_equal":   { "sql": SQL("="), "method": "get_geo_equal_sql" },
    # ... etc ...
}

# 2. Short dynamic get_geo_func:
def get_geo_sql(operator_obj, operator, field, value, params, table):
    cfg = GEO_OPERATORS.get(operator)
    if not cfg:
        raise NotImplementedError(f"Unsupported geo operator {operator}")
    return getattr(operator_obj, cfg["method"])(table, field, value, params)

# 3. Replace custom where_calc with Domain._to_sql
def where_query(model, domain, alias=None):
    q = Query(model.env, alias, model._table)
    if not domain:
        return q
    dom = Domain(domain).optimize_full(model)
    cond = dom._to_sql(model, alias, q)
    q.add_where(cond)
    return q

# 4. Extract the subquery builder
def build_geo_subquery(env, rel_model_name, ref_domain, base_alias, field, operator):
    rel_model = env[rel_model_name]
    rel_alias = f"{rel_model._table}_{env.cr.dbname[:3]}"  # or your own alias logic
    rel_q = where_query(rel_model, ref_domain, alias=rel_alias)
    op = GEO_OPERATORS[operator]["sql"]
    rel_q.add_where(f'{op}("{base_alias}"."{field}", {rel_alias}.{field})')
    sql_code = rel_q.subselect("1")
    mog = env.cr.mogrify(sql_code.code, sql_code.params).decode()
    return f"EXISTS({mog.replace('%', '%%')})"

# 5. Simplify the patched _condition_to_sql
original = Field._condition_to_sql
def _condition_to_sql(self, field_expr, operator, value, model, alias, query):
    if operator in GEO_OPERATORS:
        current = GeoOperator(model._fields[field_expr])
        params = []
        if isinstance(value, dict):
            clauses = []
            for rel, dom in value.items():
                m, col = rel.rsplit(".", 1)
                clauses.append(build_geo_subquery(
                    model.env, m, dom, alias, field_expr, operator
                ))
            return SQL(" AND ".join(clauses))
        else:
            sql_str = get_geo_sql(current, operator, field_expr, value, params, model._table)
            return SQL(sql_str, *params)
    return original(self, field_expr, operator, value, model, alias, query)
Field._condition_to_sql = _condition_to_sql
```

These changes:

- Collapse the two dicts into one.
- Remove the `match`/`case`.
- Use `Domain._to_sql` directly instead of a full copy of `where_calc`.
- Extract subquery/alias logic into `build_geo_subquery`.
</issue_to_address>

### Comment 5
<location> `base_geoengine/domains.py:39` </location>
<code_context>
+)
+
+
+def checked(self) -> DomainCondition:
+    """Validate `self` and return it if correct, otherwise raise an exception."""
+    if not isinstance(self.field_expr, str) or not self.field_expr:
</code_context>

<issue_to_address>
**issue (complexity):** Consider wrapping core DomainCondition methods to intercept only geo operators and delegate all other logic to the original implementations.

```markdown
You can drastically reduce duplication and future‐proof against Odoo core changes by wrapping (not re-implementing) the core `DomainCondition` methods. Only intercept your geo operators and defer everything else to the original implementations:

```python
from functools import wraps
from odoo.orm.domains import DomainCondition, OptimizationLevel

GEO_OPERATORS = {
    "geo_greater", "geo_lesser", "geo_equal", "geo_touch",
    "geo_within", "geo_contains", "geo_intersect",
}

# 1) Wrap checked()
_orig_checked = DomainCondition.checked
@wraps(_orig_checked)
def _checked(self):
    dc = _orig_checked(self)  # run all built‐in checks first
    if dc.operator in GEO_OPERATORS:
        # here you only do the extra geo validation/normalization
        with contextlib.suppress(Exception):
            field = dc._field(dc.model)
            if not hasattr(field, "geo_type"):
                dc._raise("Invalid geo operator on non‐geo field")
    return dc

# 2) Wrap _to_sql()
_orig_to_sql = DomainCondition._to_sql
@wraps(_orig_to_sql)
def _to_sql(self, model, alias, query):
    if self.operator in GEO_OPERATORS:
        field = self._field(model)
        model._check_field_access(field, "read")
        return field.condition_to_sql(
            self.field_expr, self.operator, self.value, model, alias, query
        )
    return _orig_to_sql(self, model, alias, query)

# 3) Wrap _optimize_step()
_orig_optimize = DomainCondition._optimize_step
@wraps(_orig_optimize)
def _optimize_step(self, model, level):
    if self.operator in GEO_OPERATORS and level < OptimizationLevel.FULL:
        # mark fully optimized & keep as-is
        optimized = DomainCondition(self.field_expr, self.operator, self.value)
        object.__setattr__(optimized, "_opt_level", OptimizationLevel.FULL)
        return optimized
    return _orig_optimize(self, model, level)

# Apply patches
DomainCondition.checked = _checked
DomainCondition._to_sql = _to_sql
DomainCondition._optimize_step = _optimize_step

# Only extend the operator set, no wholesale rewrite
from odoo.orm import domains
domains.CONDITION_OPERATORS |= GEO_OPERATORS
```

This approach

- Preserves all upstream logic (avoids bit-rot when Odoo changes core methods)  
- Only adds your geo branches  
- Keeps each wrapper tiny and focused  
- Maintains full functionality without re-implementing existing checks.
</issue_to_address>

### Comment 6
<location> `base_geoengine/domains.py:55-59` </location>
<code_context>
    if operator not in CONDITION_OPERATORS:
        # MOD
        if operator not in GEO_OPERATORS:
            self._raise("Invalid operator")
            # /MOD

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge nested if conditions ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
    if operator not in CONDITION_OPERATORS and operator not in GEO_OPERATORS:
        self._raise("Invalid operator")

```

<br/><details><summary>Explanation</summary>Too much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two `if` conditions can be combined using
`and` is an easy win.
</details>
</issue_to_address>

### Comment 7
<location> `base_geoengine/fields.py:66-70` </location>
<code_context>
    if model._active_name and active_test and model._context.get("active_test", True):
        # the item[0] trick below works for domain items and '&'/'|'/'!'
        # operators too
        if not any(item[0] == model._active_name for item in domain):
            domain = [(model._active_name, "=", 1)] + domain

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge nested if conditions ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
    if model._active_name and active_test and model._context.get("active_test", True) and not any(item[0] == model._active_name for item in domain):
        domain = [(model._active_name, "=", 1)] + domain

```

<br/><details><summary>Explanation</summary>Too much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two `if` conditions can be combined using
`and` is an easy win.
</details>
</issue_to_address>

### Comment 8
<location> `base_geoengine/fields.py:69` </location>
<code_context>
def where_calc(self, model, domain, active_test=True, alias=None):
    """
    This method is copied from base, we need to create our own query.
    """
    # if the object has an active field ('active', 'x_active'), filter out all
    # inactive records unless they were explicitly asked for
    if model._active_name and active_test and model._context.get("active_test", True):
        # the item[0] trick below works for domain items and '&'/'|'/'!'
        # operators too
        if not any(item[0] == model._active_name for item in domain):
            domain = [(model._active_name, "=", 1)] + domain

    query = Query(model.env, alias, model._table)
    if domain:
        # MOD # old expression.expression
        # _to_sql(self, model: BaseModel, alias: str, query: Query)

        # DomainCondition(self.field_expr, operator, value)
        # domain_condition = DomainCondition(domain[0][0], domain[0][1], domain[0][2])
        # optimize the domain condition
        # domain_condition = domain_condition.optimize_full(model)
        # res = domain_condition._to_sql(model=model, alias=alias, query=query)
        # return query
        # return expression.expression(domain, model, alias=alias, query=query).query

        # In Odoo 19, create Domain object and use its _to_sql method
        domain_obj = Domain(domain)
        optimized_domain = domain_obj.optimize_full(model)

        # Apply domain to query using _to_sql
        sql_condition = optimized_domain._to_sql(model, alias, query)
        query.add_where(sql_condition)

        return query

    return query

</code_context>

<issue_to_address>
**suggestion (code-quality):** Invert any/all to simplify comparisons ([`invert-any-all`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/invert-any-all/))

```suggestion
        if all(item[0] != model._active_name for item in domain):
```
</issue_to_address>

### Comment 9
<location> `base_geoengine/fields.py:121` </location>
<code_context>
def _condition_to_sql(
    self,
    field_expr: str,
    operator: str,
    value,
    model: BaseModel,
    alias: str,
    query: Query,
) -> SQL:
    """
    This method has been monkey patched in order to be able to include
    geo_operators into the Odoo search method.
    In Odoo 19, _condition_to_sql moved from BaseModel to Field.
    """

    # print(field_expr)
    # field_expr = self.name  # In Field context, self.name is the field name
    if operator in GEO_OPERATORS.keys():
        current_field = model._fields.get(field_expr)
        current_operator = GeoOperator(current_field)
        if current_field and isinstance(current_field, GeoField):
            params = []
            if isinstance(value, dict):
                # We are having indirect geo_operator like (‘geom’, ‘geo_...’,
                # {‘res.zip.poly’: [‘id’, ‘in’, [1,2,3]] })
                ref_search = value
                sub_queries = []
                for key in ref_search:
                    i = key.rfind(".")
                    rel_model_name = key[0:i]
                    rel_col = key[i + 1 :]
                    rel_model = model.env[rel_model_name]
                    # we compute the attributes search on spatial rel
                    if ref_search[key]:
                        rel_alias = (
                            rel_model._table
                            + "_"
                            + "".join(random.choices(string.ascii_lowercase, k=5))
                        )
                        rel_query = where_calc(
                            self,
                            rel_model,
                            ref_search[key],
                            active_test=True,
                            alias=rel_alias,
                        )
                        # rel_model._apply_ir_rules(rel_query, "read")
                        if operator == "geo_equal":
                            rel_query.add_where(
                                f'"{alias}"."{field_expr}" {GEO_OPERATORS[operator]} '
                                f"{rel_alias}.{rel_col}"
                            )
                        elif operator in ("geo_greater", "geo_lesser"):
                            rel_query.add_where(
                                f"ST_Area({alias}.{field_expr}) "
                                f"{GEO_OPERATORS[operator]} "
                                f"ST_Area({rel_alias}.{rel_col})"
                            )
                        else:
                            rel_query.add_where(
                                f'{GEO_OPERATORS[operator]}("{alias}"."{field_expr}", '
                                f"{rel_alias}.{rel_col})"
                            )

                        subquery_sql = rel_query.subselect("1")
                        sub_query_mogrified = (
                            model.env.cr.mogrify(subquery_sql.code, subquery_sql.params)
                            .decode("utf-8")
                            .replace(f"'{rel_model._table}'", f'"{rel_model._table}"')
                            .replace("%", "%%")
                        )
                        sub_queries.append(f"EXISTS({sub_query_mogrified})")
                query_str = " AND ".join(sub_queries)
            else:
                query_str = get_geo_func(
                    current_operator, operator, field_expr, value, params, model._table
                )
            return SQL(query_str, *params)
    return original___condition_to_sql(
        self,
        field_expr=field_expr,
        operator=operator,
        value=value,
        model=model,
        alias=alias,
        query=query,
    )

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Replace a[0:x] with a[:x] and a[x:len(a)] with a[x:] ([`remove-redundant-slice-index`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-slice-index/))
- Use set when checking membership of a collection of literals ([`collection-into-set`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/collection-into-set/))
- Low code quality found in \_condition\_to\_sql - 16% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>

The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

### Comment 10
<location> `base_geoengine/fields.py:394-403` </location>
<code_context>
    def update_db_column(self, model, column):
        """Create/update the column corresponding to ``self``.

        For creation of geo column

        :param model: an instance of the field's model
        :param column: the column's configuration (dict)
                       if it exists, or ``None``
        """
        # the column does not exist, create it

        if not column:
            create_geo_column(
                model.env.cr,
                model._table,
                self.name,
                self.geo_type.upper(),
                self.srid,
                self.dim,
                self.string,
            )
            if self.gist_index:
                create_geo_index(model.env.cr, self.name, model._table)
            return

        if column["udt_name"] == self.column_type[0]:
            if self.gist_index:
                create_geo_index(model.env.cr, self.name, model._table)
            return

        self.update_geo_db_column(model)

        if column["udt_name"] in self.column_cast_from:
            sql.convert_column(
                model.env.cr, model._table, self.name, self.column_type[1]
            )
        else:
            newname = (self.name + "_moved{}").format
            i = 0
            while sql.column_exists(model.env.cr, model._table, newname(i)):
                i += 1
            if column["is_nullable"] == "NO":
                sql.drop_not_null(model.env.cr, model._table, self.name)
            sql.rename_column(model.env.cr, model._table, self.name, newname(i))
            sql.create_column(
                model.env.cr, model._table, self.name, self.column_type[1], self.string
            )

</code_context>

<issue_to_address>
**issue (code-quality):** Extract code out into method ([`extract-method`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/extract-method/))
</issue_to_address>

### Comment 11
<location> `base_geoengine/models/geo_vector_layer.py:100` </location>
<code_context>
    @api.constrains("geo_field_id", "model_id")
    def _check_geo_field_id(self):
        for rec in self:
            if rec.model_id:
                if not rec.geo_field_id.model_id == rec.model_id:
                    raise ValidationError(
                        self.env._(
                            "The geo_field_id must be a field in %s model",
                            rec.model_id.display_name,
                        )
                    )

</code_context>

<issue_to_address>
**suggestion (code-quality):** Simplify logical expression using De Morgan identities ([`de-morgan`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/de-morgan/))

```suggestion
                if rec.geo_field_id.model_id != rec.model_id:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 27 to 38
elif use_wkb:
return wkb.loads(value, hex=True)
else:
return wkt.loads(value)
# <NIKMOD>
# 'POINT(0.0 0.0)'

try:
return wkt.loads(value)
except Exception as e:
logger.warning(_("Failed to parse WKT: %s", e)) # pylint: disable=prefer-env-translation
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Returning a default geometry on WKT parse failure may mask data issues.

Instead of returning a default geometry, raise an exception or log the error to make data issues visible and easier to debug.

logger.warning("Shapely or geojson are not available in the sys path")


def get_geo_func(current_operator, operator, left, value, params, table):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring the geo operator logic by merging mappings, using dynamic dispatch, and extracting repeated code into helpers.

Suggested change
def get_geo_func(current_operator, operator, left, value, params, table):
Most of this boilerplate can be collapsed into a few small helpers. In particular:
1. Remove the huge `match` in `get_geo_func` by dynamic dispatch.
2. Merge `GEO_OPERATORS` and `GEO_SQL_OPERATORS` into one mapping.
3. Replace your custom `where_calc` with directly using `Domain._to_sql`.
4. Factor out the repeated randomalias/subquery logic into a helper.
For example:
```python
# 1. Single OPERATOR map for both the SQL symbol and the GeoOperator method suffix
GEO_OPERATORS = {
"geo_greater": { "sql": SQL(">"), "method": "get_geo_greater_sql" },
"geo_lesser": { "sql": SQL("<"), "method": "get_geo_lesser_sql" },
"geo_equal": { "sql": SQL("="), "method": "get_geo_equal_sql" },
# ... etc ...
}
# 2. Short dynamic get_geo_func:
def get_geo_sql(operator_obj, operator, field, value, params, table):
cfg = GEO_OPERATORS.get(operator)
if not cfg:
raise NotImplementedError(f"Unsupported geo operator {operator}")
return getattr(operator_obj, cfg["method"])(table, field, value, params)
# 3. Replace custom where_calc with Domain._to_sql
def where_query(model, domain, alias=None):
q = Query(model.env, alias, model._table)
if not domain:
return q
dom = Domain(domain).optimize_full(model)
cond = dom._to_sql(model, alias, q)
q.add_where(cond)
return q
# 4. Extract the subquery builder
def build_geo_subquery(env, rel_model_name, ref_domain, base_alias, field, operator):
rel_model = env[rel_model_name]
rel_alias = f"{rel_model._table}_{env.cr.dbname[:3]}" # or your own alias logic
rel_q = where_query(rel_model, ref_domain, alias=rel_alias)
op = GEO_OPERATORS[operator]["sql"]
rel_q.add_where(f'{op}("{base_alias}"."{field}", {rel_alias}.{field})')
sql_code = rel_q.subselect("1")
mog = env.cr.mogrify(sql_code.code, sql_code.params).decode()
return f"EXISTS({mog.replace('%', '%%')})"
# 5. Simplify the patched _condition_to_sql
original = Field._condition_to_sql
def _condition_to_sql(self, field_expr, operator, value, model, alias, query):
if operator in GEO_OPERATORS:
current = GeoOperator(model._fields[field_expr])
params = []
if isinstance(value, dict):
clauses = []
for rel, dom in value.items():
m, col = rel.rsplit(".", 1)
clauses.append(build_geo_subquery(
model.env, m, dom, alias, field_expr, operator
))
return SQL(" AND ".join(clauses))
else:
sql_str = get_geo_sql(current, operator, field_expr, value, params, model._table)
return SQL(sql_str, *params)
return original(self, field_expr, operator, value, model, alias, query)
Field._condition_to_sql = _condition_to_sql

These changes:

  • Collapse the two dicts into one.
  • Remove the match/case.
  • Use Domain._to_sql directly instead of a full copy of where_calc.
  • Extract subquery/alias logic into build_geo_subquery.

original___condition_to_sql = Field._condition_to_sql


def _condition_to_sql(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): We've found these issues:


Explanation

The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

Comment on lines 394 to 403
newname = (self.name + "_moved{}").format
i = 0
while sql.column_exists(model._cr, model._table, newname(i)):
while sql.column_exists(model.env.cr, model._table, newname(i)):
i += 1
if column["is_nullable"] == "NO":
sql.drop_not_null(model._cr, model._table, self.name)
sql.rename_column(model._cr, model._table, self.name, newname(i))
sql.drop_not_null(model.env.cr, model._table, self.name)
sql.rename_column(model.env.cr, model._table, self.name, newname(i))
sql.create_column(
model._cr, model._table, self.name, self.column_type[1], self.string
model.env.cr, model._table, self.name, self.column_type[1], self.string
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Extract code out into method (extract-method)

@@ -99,7 +99,7 @@ def _check_geo_field_id(self):
if rec.model_id:
if not rec.geo_field_id.model_id == rec.model_id:

This comment was marked as off-topic.

@weinni2000 weinni2000 force-pushed the 19.0.preparation branch 4 times, most recently from 9f4e987 to f7a3db9 Compare October 4, 2025 15:33
@weinni2000
Copy link
Owner Author

not current

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant